/*
 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Autogenerated file. DO NOT EDIT

package escompat;

{%- for N, T, S in [
    ('Int8', 'byte', 1),
    ('Int16', 'short', 2),
    ('Int32', 'int', 4),
    ('BigInt64', 'long', 8),
    ('Float32', 'float', 4),
    ('Float64', 'double', 8)]
%}

{%- set elementCompat = 'number' if T != 'long' else 'BigInt' %}
{%- set asElementCompat = '%s as double as number' if elementCompat == 'number' else 'new BigInt(%s)' %}
{%- set fromElementCompat = (' as double as int as ' + T) if elementCompat == 'number' else '.getLong()' %}

/**
 * JS {{N}}Array API-compatible class
 */
export final class {{N}}Array {
    public static readonly BYTES_PER_ELEMENT = {{S}}

    /**
     * Creates an empty {{N}}Array.
     */
    public constructor() {
        this(new ArrayBuffer(0), 0, 0)
    }

    /**
     * Creates an {{N}}Array with respect to data, byteOffset and length.
     *
     * @param buf data initializer
     *
     * @param byteOffset byte offset from begin of the buf
     *
     * @param length size of elements of type {{T}} in newly created {{N}}Array
     */
    public constructor(buf: Buffer, byteOffset: number, length: number) {
        this(buf, byteOffset as int, length as int)
    }

    /**
     * Creates an {{N}}Array with respect to data, byteOffset and length.
     *
     * @param buf data initializer
     *
     * @param byteOffset byte offset from begin of the buf
     *
     * @param length size of elements of type {{T}} in newly created {{N}}Array
     */
    public constructor(buf: Buffer, byteOffset: int, length: int) {
{%- if S != "1" %}
        if (buf.getByteLength() % {{N}}Array.BYTES_PER_ELEMENT != 0) {
            throw new RangeError("ArrayBuffer.byteLength should be multiple of {{S}} as {{N}}Array.BYTES_PER_ELEMENT")
        }
        if (byteOffset % {{N}}Array.BYTES_PER_ELEMENT != 0) {
            throw new RangeError("byteOffset should be multiple of {{S}} as {{N}}Array.BYTES_PER_ELEMENT")
        }
{% endif %}
        if (this.length * {{N}}Array.BYTES_PER_ELEMENT > buf.getByteLength()) {
            throw new RangeError("Range Error: attempt to create {{N}}Array where length * {{N}}Array.BYTES_PER_ELEMENT > ArrayBuffer.byteLength")
        }
        this.byteLength = buf.getByteLength() - byteOffset
        this.byteOffset = byteOffset
        this.length = length
        this.buffer = buf
    }

    /**
     * Creates an {{N}}Array with respect to buf and byteOffset.
     *
     * @param buf data initializer
     *
     * @param byteOffset byte offset from begin of the buf
     */
    public constructor(buf: Buffer, byteOffset: int) {
        this(buf, byteOffset, buf.getByteLength() / {{N}}Array.BYTES_PER_ELEMENT)
    }

    /**
     * Creates an {{N}}Array with respect to buf and byteOffset.
     *
     * @param buf data initializer
     *
     * @param byteOffset byte offset from begin of the buf
     */
    public constructor(buf: Buffer, byteOffset: number) {
        this(buf, byteOffset as int)
    }

    /**
     * Creates an {{N}}Array with respect to buf.
     *
     * @param buf data initializer
     */
    public constructor(buf: Buffer) {
        this(buf, 0, buf.getByteLength() / {{N}}Array.BYTES_PER_ELEMENT)
    }

    /**
     * Creates an {{N}}Array with respect to length.
     *
     * @param length data initializer
     */
    public constructor(length: int) {
        this.length = length
        this.byteLength = length * {{N}}Array.BYTES_PER_ELEMENT
        this.byteOffset = 0
        this.buffer = new ArrayBuffer(this.byteLength)
    }

    /**
     * Creates an {{N}}Array with respect to length.
     *
     * @param length data initializer
     */
    public constructor(length: number) {
        this(length as int)
    }

    /**
     * Creates a copy of {{N}}Array.
     *
     * @param other data initializer
     */
    public constructor(other: {{N}}Array) {
        this.buffer = other.buffer.sliceInternal(0, other.byteLength)
        this.byteLength = other.byteLength
        this.length = other.length
    }

    /**
     * Returns an instance of primitive type at passed index.
     *
     * @param index index to look at
     *
     * @returns a primitive at index
     */
    public at(index: number): {{elementCompat}} {
        return {{asElementCompat % 'this.at(index as int)'}}
    }

    /**
     * Returns an instance of primitive type at passed index.
     *
     * @param index index to look at
     *
     * @returns a primitive at index
     */
    public at(index: int): {{T}} {
        if (index < 0) {
            index = this.length + index
        }
        let byteIndex = index * {{N}}Array.BYTES_PER_ELEMENT + this.byteOffset
        {%- if T == "float" %}
        {%- set ET = "int" %}
        {%- elif T == "double" %}
        {%- set ET = "long" %}
        {%- else %}
        {%- set ET = T %}
        {%- endif %}
        let res: {{ET}} = 0
        for (let i = 0; i < {{N}}Array.BYTES_PER_ELEMENT; ++i) {
            let byteVal = this.buffer.at(byteIndex + i) as {{ET}}
            {%- if ET != "byte" %}
            byteVal &= 0xff
            {%- endif %}
            res = (res | byteVal << (8 * i)) as {{ET}};
        }
        {%- if T == "float" %}
        return Float.bitCastFromInt(res)
        {%- elif T == "double" %}
        return Double.bitCastFromLong(res)
        {%- else %}
        return res
        {%- endif %}
    }

    /**
     * Makes a copy of internal elements to insertPos from startPos to endPos.
     *
     * @param insertPos insert index to place copied elements
     *
     * @param startPos start index to begin copy from
     *
     * @param endPos last index to end copy from, excluded
     *
     * See rules of parameters normalization on {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin | MDN}
     */
    public copyWithin(insertPos: number, startPos: number, endPos: number): void {
        this.copyWithin(insertPos as int, startPos as int, endPos as int)
    }

    /**
     * Makes a copy of internal elements to insertPos from startPos to endPos.
     *
     * @param insertPos insert index to place copied elements
     *
     * @param startPos start index to begin copy from
     *
     * @param endPos last index to end copy from, excluded
     *
     * See rules of parameters normalization on {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin | MDN}
     */
    public copyWithin(insertPos: int, startPos: int, endPos: int): void {
        if (insertPos < -this.length) {
            insertPos = 0
        }
        if (insertPos >= this.length) {
            return
        }
        if (insertPos < 0) {
            insertPos = this.length + insertPos
        }

        if (startPos < -this.length) {
            startPos = 0
        }
        if (startPos >= this.length) {
            return
        }
        if (startPos < 0) {
            startPos = this.length + startPos
        }

        if (endPos < -this.length) {
            endPos = 0
        }
        if (endPos < 0) {
            endPos = this.length + endPos
        }
        if (endPos > this.length) {
            endPos = this.length
        }
        if (endPos < startPos) {
            return
        }

        for (let i = startPos; i < endPos; ++i) {
            this.set(insertPos + i - startPos, this.at(i))
        }
    }

    /**
     * Makes a copy of internal elements to insertPos from startPos to end of {{N}}Array.
     *
     * @param insertPos insert index to place copied elements
     *
     * @param startPos start index to begin copy from
     *
     * See rules of parameters normalization {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin}
     */
    public copyWithin(insertPos: number, startPos: number): void {
        this.copyWithin(insertPos as int, startPos as int)
    }

    /**
     * Makes a copy of internal elements to insertPos from startPos to end of {{N}}Array.
     *
     * @param insertPos insert index to place copied elements
     *
     * @param startPos start index to begin copy from
     *
     * See rules of parameters normalization {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin}
     */
    public copyWithin(insertPos: int, startPos: int): void {
        this.copyWithin(insertPos, startPos, this.length)
    }

    /**
     * Makes a copy of internal elements to insertPos from begin to end of {{N}}Array.
     *
     * See rules of parameters normalization:
     * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin}
     */
    public copyWithin(insertPos: number): void {
        this.copyWithin(insertPos as int)
    }

    /**
     * Makes a copy of internal elements to insertPos from begin to end of {{N}}Array.
     *
     * See rules of parameters normalization:
     * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin}
     */
    public copyWithin(insertPos: int): void {
        this.copyWithin(insertPos, 0, this.length)
    }

    {% set TBoxed = T[0].upper() + T[1:] -%}

    /**
     * Returns an iterator for all entries
     */
    /* public */ internal entries(): MapIterator<Int, {{TBoxed}}> {
        let ret: Entry<Int, {{TBoxed}}>[] = new Entry<Int, {{TBoxed}}>[this.length];
        for (let i: int = 0; i < this.length; i++) {
            ret[i] = new Entry<Int, {{TBoxed}}>(i, this.at(i));
        }
        return new MapIterator<Int, {{TBoxed}}>(ret);
    }

    /**
     * Fills the {{N}}Array with specified value
     *
     * @param value new valuy
     *
     * @returns modified {{N}}Array
     */
    public fill(value: {{elementCompat}}, start: number, end: number): {{N}}Array {
        return this.fill(value{{fromElementCompat}}, start as int, end as int)
    }

    /**
     * Fills the {{N}}Array with specified value
     *
     * @param value new valuy
     *
     * @returns modified {{N}}Array
     */
    public fill(value: {{T}}, start: int, end: int): {{N}}Array {
        if (start < -this.length) {
            start = 0
        }
        if (start < 0) {
            start += this.length
        }
        if (start >= this.length) {
            return this;
        }
        if (end < -this.length) {
            end = 0
        }
        if (end < 0) {
            end += this.length
        }
        if (end >= this.length) {
            end = this.length
        }
        for (let i = start; i < end; ++i) {
            this.set(i, value)
        }
        return this;
    }

    /**
     * Fills the {{N}}Array with specified value
     *
     * @param value new valuy
     *
     * @returns modified {{N}}Array
     */
    public fill(value: {{elementCompat}}, start: number): {{N}}Array {
        return this.fill(value{{fromElementCompat}}, start as int)
    }

    /**
     * Fills the {{N}}Array with specified value
     *
     * @param value new valuy
     *
     * @returns modified {{N}}Array
     */
    public fill(value: {{T}}, start: int): {{N}}Array {
        return this.fill(value, start, this.length)
    }

    /**
     * Fills the {{N}}Array with specified value
     *
     * @param value new valuy
     *
     * @returns modified {{N}}Array
     */
    public fill(value: {{T}}): {{N}}Array {
        return this.fill(value, 0, this.length)
    }

    /**
     * Assigns val as element on insertPos.
     * @description Added to avoid (un)packing a single value into array to use overloaded set({{T}}[], insertPos)
     *
     * @param val value to set
     *
     * @param insertPos index to change
     */
    public set(insertPos: number, val: {{elementCompat}}): void {
        this.set(insertPos as int, val{{fromElementCompat}})
    }

    /**
     * Assigns val as element on insertPos.
     * @description Added to avoid (un)packing a single value into array to use overloaded set({{T}}[], insertPos)
     *
     * @param val value to set
     *
     * @param insertPos index to change
     */
    public set(insertPos: int, val: {{T}}): void {
        if (insertPos < 0 || insertPos >= this.length) {
            throw new RangeError("set: insertPos is out-of-bounds")
        }

        let startByte = insertPos * {{N}}Array.BYTES_PER_ELEMENT + this.byteOffset
        {%- if T == "float" %}
        let bits = Float.bitCastToInt(val);
        {%- elif T == "double" %}
        let bits = Double.bitCastToLong(val);
        {%- else %}
        let bits = val;
        {%- endif %}
        for (let i = 0; i < {{N}}Array.BYTES_PER_ELEMENT; ++i) {
            let byteVal = ((bits >>> (i * 8)) & 0xff) as byte
            this.buffer.set(startByte + i, byteVal)
        }
    }

    /**
     * Copies all elements of arr to the current {{N}}Array starting from insertPos.
     *
     * @param arr array to copy data from
     *
     * @param insertPos start index where data from arr will be inserted
     *
     * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set}
     */
    public set(arr: {{elementCompat}}[], insertPos1: number): void {
        const insertPos = insertPos1 as int
        if (insertPos < 0 || insertPos + arr.length > this.length) {
            throw new RangeError("set(insertPos: int, arr: {{T}}[]): size of arr is greater than {{N}}Array.length")
        }
        for (let i = 0; i < arr.length; ++i) {
            this.set(insertPos + i, arr[i]{{fromElementCompat}})
        }
    }

    /**
     * Copies all elements of arr to the current {{N}}Array starting from insertPos.
     *
     * @param arr array to copy data from
     *
     * @param insertPos start index where data from arr will be inserted
     *
     * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set}
     */
    public set(arr: {{T}}[], insertPos: int): void {
        if (insertPos < 0 || insertPos + arr.length > this.length) {
            throw new RangeError("set(insertPos: int, arr: {{T}}[]): size of arr is greater than {{N}}Array.length")
        }
        for (let i = 0; i < arr.length; ++i) {
            this.set(insertPos + i, arr[i])
        }
    }

{% if T != 'double' %}
    /**
     * Copies all elements of arr to the current {{N}}Array.
     *
     * @param arr array to copy data from
     */
    public set(arr: {{elementCompat}}[]): void {
        this.set(arr, 0)
    }
{%- endif%}

    /**
     * Copies all elements of arr to the current {{N}}Array.
     *
     * @param arr array to copy data from
     */
    public set(arr: {{T}}[]): void {
        this.set(arr, 0)
    }

    /**
     * Creates an {{N}}Array from array-like argument
     *
     * @param o array-like object to initialize {{N}}Array
     *
     * @param mapFn function to apply for each
     *
     * @returns new {{N}}Array
     */
    public from(o: Object, mapFn: (e: Object) => {{T}}): {{N}}Array {
        let newF: (e: Object, index: int) => {{T}} =
            (e: Object, index: int): {{T}} => { return mapFn(e) }
        return this.from(o, newF)
    }

    /**
     * Creates an {{N}}Array from array-like argument
     *
     * @param o array-like object to initialize {{N}}Array
     *
     * @returns new {{N}}Array
     */
    public from(o: Object): {{N}}Array {
        throw new Error("{{N}}Array.from: not implemented")
    }

    /**
     * Checks if specified argument is in {{N}}Array
     *
     * @param e search element
     *
     * @param fromIndex start index to search from
     *
     * @returns true if e is in {{N}}Array, false otherwise
     */
    public includes(e: {{elementCompat}}, fromIndex: number): boolean {
        return this.includes(e{{fromElementCompat}}, fromIndex as int)
    }

    /**
     * Checks if specified argument is in {{N}}Array
     *
     * @param e search element
     *
     * @param fromIndex start index to search from
     *
     * @returns true if e is in {{N}}Array, false otherwise
     */
    public includes(e: {{T}}, fromIndex: int): boolean {
        for (let i = fromIndex; i < this.length; ++i) {
            if (this.at(i) == e) {
                return true
            }
        }
        return false
    }

{% if T != 'double' %}
    /**
     * Checks if specified argument is in {{N}}Array
     *
     * @param e search element
     *
     * @returns true if e is in {{N}}Array, false otherwise
     */
    public includes(e: {{elementCompat}}): boolean {
        return this.includes(e{{fromElementCompat}}, 0)
    }
{%- endif %}

    /**
     * Checks if specified argument is in {{N}}Array
     *
     * @param e search element
     *
     * @returns true if e is in {{N}}Array, false otherwise
     */
    public includes(e: {{T}}): boolean {
        return this.includes(e, 0)
    }

    /**
     * Returns index of specified element
     *
     * @param e search element
     *
     * @param fromIndex start index to search from
     *
     * @returns index of element if it presents, -1 otherwise
     */
    public indexOf(e: {{elementCompat}}, fromIndex: number): number {
        return this.indexOf(e{{fromElementCompat}}, fromIndex as int)
    }

    /**
     * Returns index of specified element
     *
     * @param e search element
     *
     * @param fromIndex start index to search from
     *
     * @returns index of element if it presents, -1 otherwise
     */
    public indexOf(e: {{T}}, fromIndex: int): int {
        if (fromIndex >= this.length) {
            return -1
        }
        if (fromIndex < 0) {
            fromIndex += this.length
            // See TypedArray.indexOf JS docs
            if (fromIndex < 0) {
                fromIndex = 0
            }
        }
        for (let i = fromIndex; i < this.length; ++i) {
            if (this.at(i) == e) {
                return i
            }
        }
        return -1
    }

{% if T != 'double' %}
    /**
     * Returns index of specified element
     *
     * @param e search element
     *
     * @returns index of element if it presents, -1 otherwise
     */
    public indexOf(e: {{elementCompat}}): number {
        return this.indexOf(e{{fromElementCompat}})
    }
{%- endif %}

    /**
     * Returns index of specified element
     *
     * @param e search element
     *
     * @returns index of element if it presents, -1 otherwise
     */
    public indexOf(e: {{T}}): int {
        return this.indexOf(e, 0)
    }

    /**
     * Joins data to a string
     *
     * @param s separator
     *
     * @returns joined representation
     */
    public join(s: string): string {
        let res: StringBuilder = new StringBuilder("")
        for (let i = 0; i < this.length - 1; ++i) {
            res.append(this.at(i))
            res.append(s)
        }
        if (this.length > 0) {
            res.append(this.at(this.length - 1))
        }
        return res.toString()
    }

    /**
     * Joins data to a string
     *
     * @returns joined representation with comma separator
     */
    public join(): string {
        return this.join(",")
    }

    /**
     * Returns keys of the {{N}}Array
     *
     * @returns iterator over keys
     */
    public keys(): ValuesIterator<Int> {
        let ret: Int[] = new Int[this.length];
        for (let i: int = 0; i < this.length; i++) {
            ret[i] = i;
        }
        return new ValuesIterator<Int>(ret);
    }

    /**
     * Moves backwards starting at fromIndex to 0 and search val.
     *
     * @param val a value to search
     *
     * @param fromIndex the first index to search val at, i.e. fromIndex is included in search space
     *
     * @returns right-most index of val. It must be less or equal than fromIndex. -1 if val not found
     *
     * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/lastIndexOf
     */
    public lastIndexOf(val: {{elementCompat}}, fromIndex: number): number {
        return this.lastIndexOf(val{{fromElementCompat}}, fromIndex as int)
    }

    /**
     * Moves backwards starting at fromIndex to 0 and search val.
     *
     * @param val a value to search
     *
     * @param fromIndex the first index to search val at, i.e. fromIndex is included in search space
     *
     * @returns right-most index of val. It must be less or equal than fromIndex. -1 if val not found
     *
     * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/lastIndexOf
     */
    public lastIndexOf(val: {{T}}, fromIndex: int): int {
        if (fromIndex >= this.length) {
            fromIndex = this.length - 1
        }
        if (fromIndex < 0) {
            fromIndex = this.length + fromIndex
        }

        for (let i = fromIndex; i >= 0; --i) {
            if (this.at(i) == val) {
                return i
            }
        }
        return -1
    }

{% if T != 'double' %}
    /**
     * Moves backwards and search val.
     *
     * @param val a value to search
     *
     * @returns right-most index of val. -1 if val not found
     */
    public lastIndexOf(val: {{elementCompat}}): number {
        return this.lastIndexOf(val{{fromElementCompat}}, this.length - 1)
    }
{%- endif %}

    /**
     * Moves backwards and search val.
     *
     * @param val a value to search
     *
     * @returns right-most index of val. -1 if val not found
     */
    public lastIndexOf(val: {{T}}): int {
        return this.lastIndexOf(val, this.length - 1)
    }

   /**
    * Creates a new {{N}}Array using initializer
    *
    * @param data initializer
    *
    * @returns a new {{N}}Array from data
    */
    public of(data: Object[]): {{N}}Array {
        throw new Error("{{N}}Array.of: not implemented")
    }

    /**
     * Creates a new {{N}}Array using reversed data from the current one
     *
     * @returns a new {{N}}Array using reversed data from the current one
     */
    public reverse(): {{N}}Array {
        let res = new {{N}}Array(this)
        for (let i = 0; i < this.length; ++i) {
            res.set(this.length - 1 - i, this.at(i))
        }
        return res
    }

    /**
     * Creates a slice of current {{N}}Array using range [begin, end)
     *
     * @param begin start index to be taken into slice
     *
     * @param end last index to be taken into slice
     *
     * @returns a new {{N}}Array with elements of current {{N}}Array[begin;end) where end index is excluded
     *
     * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice
     */
     public slice(begin: number, end: number): {{N}}Array {
        return this.slice(begin as int, end as int)
    }

    /**
     * Creates a slice of current {{N}}Array using range [begin, end)
     *
     * @param begin start index to be taken into slice
     *
     * @param end last index to be taken into slice
     *
     * @returns a new {{N}}Array with elements of current {{N}}Array[begin;end) where end index is excluded
     *
     * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice
     */
    public slice(begin: int, end: int): {{N}}Array {
        let buf = this.buffer.sliceInternal(begin * {{N}}Array.BYTES_PER_ELEMENT, end * {{N}}Array.BYTES_PER_ELEMENT)
        return new {{N}}Array(buf)
    }

    /**
     * Creates a slice of current {{N}}Array using range [begin, this.length).
     *
     * @param begin start index to be taken into slice
     *
     * @returns a new {{N}}Array with elements of current {{N}}Array[begin, this.length)
     */
    public slice(begin: number): {{N}}Array {
        return this.slice(begin as int)
    }

    /**
     * Creates a slice of current {{N}}Array using range [begin, this.length).
     *
     * @param begin start index to be taken into slice
     *
     * @returns a new {{N}}Array with elements of current {{N}}Array[begin, this.length)
     */
    public slice(begin: int): {{N}}Array {
        return this.slice(begin, this.length)
    }

    /**
     * Creates a slice of current {{N}} with all elements.
     *
     * @returns a new {{N}}Array with elements of current {{N}}Array
     */
    public slice(): {{N}}Array {
        return new {{N}}Array(this)
    }

    /**
     * Sorts in-place according to the numeric ordering
     *
     * @returns sorted {{N}}Array
     */
    public sort(): {{N}}Array {
        let newF: (a: {{elementCompat}}, b: {{elementCompat}}) => number = (a: {{elementCompat}}, b: {{elementCompat}}): number => {
            const a1 = a{{fromElementCompat}}
            const b1 = b{{fromElementCompat}}
            return a1 < b1 ? -1 : a1 == b1 ? 0 : 1
        }
        return this.sort(newF)
    }

    /**
     * Creates a {{N}}Array with the same underlying ArrayBuffer
     *
     * @param begin start index, inclusive
     *
     * @param end last index, exclusive
     *
     * @returns new {{N}}Array with the same underlying ArrayBuffer
     */
    public subarray(begin: number, end: number): {{N}}Array {
        return this.subarray(begin as int, end as int)
    }

    /**
     * Creates a {{N}}Array with the same underlying ArrayBuffer
     *
     * @param begin start index, inclusive
     *
     * @param end last index, exclusive
     *
     * @returns new {{N}}Array with the same underlying ArrayBuffer
     */
    public subarray(begin: int, end: int): {{N}}Array {
        return new {{N}}Array(this.buffer, begin * {{N}}Array.BYTES_PER_ELEMENT, end - begin)
    }

    /**
     * Creates a {{N}}Array with the same ArrayBuffer
     *
     * @param begin start index, inclusive
     *
     * @returns new {{N}}Array with the same ArrayBuffer
     */
    public subarray(begin: number): {{N}}Array {
        return this.subarray(begin as int)
    }

    /**
     * Creates a {{N}}Array with the same ArrayBuffer
     *
     * @param begin start index, inclusive
     *
     * @returns new {{N}}Array with the same ArrayBuffer
     */
    public subarray(begin: int): {{N}}Array {
        return this.subarray(begin, this.length)
    }

    /**
     * Creates a {{N}}Array with the same ArrayBuffer
     *
     * @returns new {{N}}Array with the same ArrayBuffer
     */
    public subarray(): {{N}}Array {
        return this.subarray(0, this.length)
    }

    /**
     * Converts {{N}}Array to a string with respect to locale
     *
     * @param locales
     *
     * @param options
     *
     * @returns string representation
     */
    public toLocaleString(locales: Object, options: Object): string {
        throw new Error("{{N}}Array.toLocaleString: not implemented")
    }

    /**
     * Converts {{N}}Array to a string with respect to locale
     *
     * @param locales
     *
     * @returns string representation
     */
    public toLocaleString(locales: Object): string {
        return this.toLocaleString(new Object(), new Object())
    }

    /**
     * Converts {{N}}Array to a string with respect to locale
     *
     * @returns string representation
     */
    public toLocaleString(): string {
        return this.toLocaleString(new Object(), new Object())
    }

    /**
     * Creates a reversed copy
     *
     * @returns a reversed copy
     */
    public toReversed(): {{N}}Array {
        return new {{N}}Array(this).reverse()
    }

    /**
     * Creates a sorted copy
     *
     * @returns a sorted copy
     */
    public toSorted(): {{N}}Array {
        return new {{N}}Array(this).sort()
    }

    /**
     * Returns a string representation of the {{N}}Array
     *
     * @returns a string representation of the {{N}}Array
     */
    public override toString(): string {
        let res = new StringBuilder();
        for (let i = 0; i < this.length - 1; ++i) {
            res.append(this.at(i))
            res.append(c',')
        }
        if (this.length > 0) {
            res.append(this.at(this.length - 1))
        }
        return res.toString()
    }

    /**
     * Returns array values iterator
     *
     * @returns an iterator
     */
    public values(): ValuesIterator<{{TBoxed}}> {
        const ret = new {{TBoxed}}[this.length];
        for (let i: int = 0; i < this.length; i++) {
            ret[i] = this.at(i);
        }
        return new ValuesIterator<{{TBoxed}}>(ret);
    }

    /**
     * Creates a copy with replaced value on index
     *
     * @param index
     *
     * @param value
     *
     * @returns an {{N}}Array with replaced value on index
     */
    public with(index: number, value: {{elementCompat}}): {{N}}Array {
        return this.with(index as int, value{{fromElementCompat}})
    }

    /**
     * Creates a copy with replaced value on index
     *
     * @param index
     *
     * @param value
     *
     * @returns an {{N}}Array with replaced value on index
     */
    public with(index: int, value: {{T}}): {{N}}Array {
        let res = new {{N}}Array(this)
        res.set(index, value)
        return res
    }

    /// === with element lambda functions ===
    {%- for idxType, castToIdx in [('int', ''), ('number', ' as double as number')] %}
    {%- set skip = False %}
    {%- if idxType == 'int' %}
    {%- set elType, castToEl, castFromEl = T, '%s', '' %}
    {%- set skip = T == 'long' or T == 'double' %}
    {%- else %}
    {%- set elType, castToEl, castFromEl = elementCompat, asElementCompat, fromElementCompat %}
    {%- endif %}

    {%- if not skip %}
    /**
     * Checks that all elements of {{N}}Array satisfy the passed function
     *
     * @param fn check function
     *
     * @returns true if all elements satisfy fn
     */
    public every(fn: (element: {{elType}}) => boolean): boolean {
        let newF: (element: {{T}}, index: int, array: {{N}}Array) => boolean =
            (element: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'element'}}) }
        return this.every(newF)
    }
    /**
     * creates a new {{N}}Array from current {{N}}Array based on a condition fn
     *
     * @param fn the condition to apply for each element
     *
     * @returns a new {{N}}Array with elements from current {{N}}Array that satisfy condition fn
     */
    public filter(fn: (val: {{elType}}) => boolean): {{N}}Array {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => boolean =
            (val: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'val'}}) }
        return this.filter(newF)
    }

    /**
     * Finds the first element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the first element that satisfies fn
     */
    public find(fn: (val: {{elType}}) => boolean): {{T}} {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => boolean =
            (val: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'val'}}) }
        return this.find(newF)
    }

    /**
     * Finds an index of the first element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the index of the first element that satisfies fn
     */
    public findIndex(fn: (val: {{elType}}) => boolean): {{idxType}} {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => boolean =
            (val: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'val'}}) }
        return this.findIndex(newF){{castToIdx}}
    }

    /**
     * Finds the last element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the last element that satisfies fn
     */
    public findLast(fn: (val: {{elType}}) => boolean): {{elType}} {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => boolean =
            (val: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'val'}}) }
        return {{castToEl % 'this.findLast(newF)'}}
    }

    /**
     * Finds an index of the last element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the index of the last element that satisfies fn, -1 otherwise
     */
    public findLastIndex(fn: (val: {{elType}}) => boolean): {{idxType}} {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => boolean =
            (val: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'val'}}) }
        return this.findLastIndex(newF){{castToIdx}}
    }

    /**
     * Applies a function over all elements of {{N}}Array
     *
     * @param fn function to apply
     *
     * @returns undefined
     */
    public forEach(fn: (val: {{elType}}) => {{T}}): void {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => {{T}} =
            (val: {{T}}, index: int, array: {{N}}Array): {{T}} => { return fn({{castToEl % 'val'}}) }
        this.forEach(newF)
    }

   /**
    * Creates a new {{N}}Array using fn(arr[i]) over all elements of current {{N}}Array
    *
    * @param fn a function to apply for each element of current {{N}}Array
    *
    * @returns a new {{N}}Array where for each element from current {{N}}Array fn was applied
    */
    public map(fn: (val: {{elType}}) => {{elType}}): {{N}}Array {
        let newF: (val: {{T}}, index: int) => {{T}} =
            (val: {{T}}, index: int): {{T}} => { return fn({{castToEl % 'val'}}){{castFromEl}} }
        return this.map(newF)
    }

    /**
     * Checks that at least one element of {{N}}Array satisfies the passed function
     *
     * @param fn check function
     *
     * @returns true if some element satisfies fn
     */
    public some(fn: (element: {{elType}}) => boolean): boolean {
        let newF: (element: {{T}}, index: int, array: {{N}}Array) => boolean =
            (element: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'element'}}) }
        return this.some(newF)
    }

    // TODO(kprokopenko): this may be not skipped
    /**
     * Sorts in-place
     *
     * @param fn comparator
     *
     * @returns sorted {{N}}Array
     */
    public sort(fn: (a: {{elType}}, b: {{elType}}) => {{idxType}}): {{N}}Array {
        let arr: {{T}}[] = new {{T}}[this.length]
        for (let i = 0; i < this.length; ++i) {
            arr[i] = this.at(i)
        }
        // TODO(ivan-tyulyandin): unresolved reference i in for loop, blocked by internal issue 12961
        /*
            let mustPrecede: (a: {{T}}, b: {{T}}) => boolean =
                (a: {{T}}, b: {{T}}): boolean => { return (fn(a, b) <= 0) }
            sort(arr, mustPrecede)
            for (let i = 0; i < this.length; ++i) {
                this.set(i, arr[i])
            }
        */
        return this;
    }
    {%- endif %}

    /**
     * Checks that at least one element of {{N}}Array satisfies the passed function
     *
     * @param fn check function
     *
     * @returns true if some element satisfies fn
     */
    public some(fn: (element: {{elType}}, index: {{idxType}}, array: {{N}}Array) => boolean): boolean {
        for (let i = 0; i < this.length; ++i) {
            if (fn({{castToEl % 'this.at(i)'}}, i{{castToIdx}}, this)) {
                return true
            }
        }
        return false
    }

    /**
     * Checks that at least one element of {{N}}Array satisfies the passed function
     *
     * @param fn check function
     *
     * @returns true if some element satisfies fn
     */
    public some(fn: (element: {{elType}}, index: {{idxType}}) => boolean): boolean {
        let newF: (element: {{T}}, index: int, array: {{N}}Array) => boolean =
            (element: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'element'}}, index{{castToIdx}}) }
        return this.some(newF)
    }

    /**
     * Reduces data into a single value using left-to-right traversal
     *
     * @param fn condition
     *
     * @param init initial value
     *
     * @returns reduction result
     */
    public reduce(fn: (acc: {{elType}}, curVal: {{elType}}, curIndex: {{idxType}}, array: {{N}}Array) => {{elType}}, init: {{elType}}): {{elType}} {
        let acc = init
        for (let i = 0; i < this.length; ++i) {
            acc = fn(acc, {{castToEl % 'this.at(i)'}}, i{{castToIdx}}, this)
        }
        return acc
    }

    /**
     * Reduces data into a single value using left-to-right traversal
     *
     * @param fn condition
     *
     * @returns reduction result
     */
    public reduce(fn: (acc: {{elType}}, curVal: {{elType}}, curIndex: {{idxType}}, array: {{N}}Array) => {{elType}}): {{elType}} {
        let acc = {{castToEl % 'this.at(0)'}}
        for (let i = 1; i < this.length; ++i) {
            acc = fn(acc, {{castToEl % 'this.at(i)'}}, i{{castToIdx}}, this)
        }
        return acc
    }

    /**
     * Reduces data into a single value using right-to-left traversal
     *
     * @param fn condition
     *
     * @param init initial value
     *
     * @returns reduction result
     */
    public reduceRight(fn: (acc: {{elType}}, curVal: {{elType}}, curIndex: {{idxType}}, array: {{N}}Array) => {{elType}}, init: {{elType}}): {{elType}} {
        let acc = {{castToEl % 'init'}}
        for (let i = this.length - 1; i >= 0; --i) {
            acc = fn(acc, {{castToEl % 'this.at(i)'}}, i{{castToIdx}}, this)
        }
        return acc
    }

    /**
     * Reduces data into a single value using right-to-left traversal
     *
     * @param fn condition
     *
     * @returns reduction result
     */
    public reduceRight(fn: (acc: {{elType}}, curVal: {{elType}}, curIndex: {{idxType}}, array: {{N}}Array) => {{elType}}): {{elType}} {
        let acc: {{elType}} = {{castToEl % 'this.at(this.length - 1)'}}
        for (let i = this.length - 2; i >= 0; --i) {
            acc = fn(acc, {{castToEl % 'this.at(i)'}}, i{{castToIdx}}, this)
        }
        return acc
    }

    /**
    * Creates a new {{N}}Array using fn(arr[i]) over all elements of current {{N}}Array.
    *
    * @param fn a function to apply for each element of current {{N}}Array
    *
    * @returns a new {{N}}Array where for each element from current {{N}}Array fn was applied
    */
    public map(fn: (val: {{elType}}, index: {{idxType}}) => {{elType}}): {{N}}Array {
        let resBuf = new ArrayBuffer(this.length * {{N}}Array.BYTES_PER_ELEMENT)
        let res = new {{N}}Array(resBuf)
        for (let i = 0; i < this.length; ++i) {
            res.set(i, fn({{castToEl % 'this.at(i)'}}, i{{castToIdx}}){{castFromEl}})
        }
        return res
    }

    /**
     * Checks that all elements of {{N}}Array satisfy the passed function
     *
     * @param fn check function
     *
     * @returns true if all elements satisfy fn
     */
    public every(fn: (element: {{elType}}, index: {{idxType}}, array: {{N}}Array) => boolean): boolean {
        for (let i = 0; i < this.length; ++i) {
            if (!fn({{castToEl % 'this.at(i)'}}, i{{castToIdx}}, this)) {
                return false
            }
        }
        return true
    }

    /**
     * Checks that all elements of {{N}}Array satisfy the passed function
     *
     * @param fn check function
     *
     * @returns true if all elements satisfy fn
     */
    public every(fn: (element: {{elType}}, index: {{idxType}}) => boolean): boolean {
        let newF: (element: {{T}}, index: int, array: {{N}}Array) => boolean =
            (element: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'element'}}, index{{castToIdx}}) }
        return this.every(newF)
    }

    /**
     * Creates a new {{N}}Array from current {{N}}Array based on a condition fn.
     *
     * @param fn the condition to apply for each element
     *
     * @returns a new {{N}}Array with elements from current {{N}}Array that satisfy condition fn
     */
    public filter(fn: (val: {{elType}}, index: {{idxType}}, array: {{N}}Array) => boolean): {{N}}Array {
        let markers = new boolean[this.length]
        let resLen = 0
        for (let i = 0; i < this.length; ++i) {
            markers[i] = fn({{castToEl % 'this.at(i)'}}, i{{castToIdx}}, this)
            if (markers[i]) {
                ++resLen
            }
        }
        let resBuf = new ArrayBuffer(resLen * {{N}}Array.BYTES_PER_ELEMENT)
        let res = new {{N}}Array(resBuf)
        for (let i = 0, j = 0; i < this.length; ++i) {
            if (markers[i]) {
                res.set(j, this.at(i))
                ++j
            }
        }
        return res
    }

    /**
     * creates a new {{N}}Array from current {{N}}Array based on a condition fn
     *
     * @param fn the condition to apply for each element
     *
     * @returns a new {{N}}Array with elements from current {{N}}Array that satisfy condition fn
     */
    public filter(fn: (val: {{elType}}, index: {{idxType}}) => boolean): {{N}}Array {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => boolean =
            (val: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'val'}}, index{{castToIdx}}) }
        return this.filter(newF)
    }

    /**
     * Finds the first element in the {{N}}Array that satisfies the condition
     *
     * @param fn the condition to apply for each element
     *
     * @returns the first element that satisfies fn
     * TODO: return {{T}} | undefined as in JS
     */
    public find(fn: (val: {{elType}}, index: {{idxType}}, array: {{N}}Array) => boolean): {{T}} {
        for (let i = 0; i < this.length; ++i) {
            let val = this.at(i)
            if (fn({{castToEl % 'val'}}, i{{castToIdx}}, this)) {
                return val
            }
        }
        throw new Error("{{N}}Array.find: not implemented if element was not found")
    }

    /**
     * Finds the first element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the first element that satisfies fn
     */
    public find(fn: (val: {{elType}}, index: {{idxType}}) => boolean): {{T}} {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => boolean =
            (val: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'val'}}, index{{castToIdx}}) }
        return this.find(newF)
    }

    /**
     * Finds an index of the first element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the index of the first element that satisfies fn
     */
    public findIndex(fn: (val: {{elType}}, index: {{idxType}}, array: {{N}}Array) => boolean): {{idxType}} {
        for (let i = 0; i < this.length; ++i) {
            let val = this.at(i)
            if (fn({{castToEl % 'val'}}, i{{castToIdx}}, this)) {
                return i{{castToIdx}}
            }
        }
        return -1{{castToIdx}}
    }

    /**
     * Finds an index of the first element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the index of the first element that satisfies fn
     */
    public findIndex(fn: (val: {{elType}}, index: {{idxType}}) => boolean): {{idxType}} {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => boolean =
            (val: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'val'}}, index{{castToIdx}}) }
        return this.findIndex(newF){{castToIdx}}
    }

    /**
     * Finds the last element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the last element that satisfies fn
     */
    public findLast(fn: (val: {{elType}}, index: {{idxType}}, array: {{N}}Array) => boolean): {{T}} {
        for (let i = this.length - 1; i >= 0; --i) {
            let val = this.at(i)
            if (fn({{castToEl % 'val'}}, i{{castToIdx}}, this)) {
                return val
            }
        }
        throw new Error("{{N}}Array.findLast: not implemented if an element was not found")
    }

    /**
     * Finds the last element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the last element that satisfies fn
     */
    public findLast(fn: (val: {{elType}}, index: {{idxType}}) => boolean): {{T}} {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => boolean =
            (val: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'val'}}, index{{castToIdx}}) }
        return this.findLast(newF)
    }

    /**
     * Finds an index of the last element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the index of the last element that satisfies fn, -1 otherwise
     */
    public findLastIndex(fn: (val: {{elType}}, index: {{idxType}}, array: {{N}}Array) => boolean): {{idxType}} {
        for (let i = this.length - 1; i >= 0; --i) {
            let val = this.at(i)
            if (fn({{castToEl % 'val'}}, i{{castToIdx}}, this)) {
                return i
            }
        }
        return -1{{castToIdx}}
    }

    /**
     * Finds an index of the last element in the {{N}}Array that satisfies the condition
     *
     * @param fn condition
     *
     * @returns the index of the last element that satisfies fn, -1 otherwise
     */
    public findLastIndex(fn: (val: {{elType}}, index: {{idxType}}) => boolean): {{idxType}} {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => boolean =
            (val: {{T}}, index: int, array: {{N}}Array): boolean => { return fn({{castToEl % 'val'}}, index{{castToIdx}}) }
        return this.findLastIndex(newF){{castToIdx}}
    }

    /**
     * Applies a function over all elements of {{N}}Array
     *
     * @param fn function to apply
     *
     * @returns undefined
     */
    public forEach(fn: (val: {{elType}}, index: {{idxType}}, array: {{N}}Array) => {{T}}): void {
        for (let i = 0; i < this.length; ++i) {
            this.set(i, fn({{castToEl % 'this.at(i)'}}, i{{castToIdx}}, this))
        }
        throw new Error("{{N}}Array.forEach: has to return undefined, but returns void for now")
    }

    /**
     * Applies a function over all elements of {{N}}Array
     *
     * @param fn function to apply
     *
     * @returns undefined
     */
    public forEach(fn: (val: {{elType}}, index: {{idxType}}) => {{T}}): void {
        let newF: (val: {{T}}, index: int, array: {{N}}Array) => {{T}} =
            (val: {{T}}, index: int, array: {{N}}Array): {{T}} => { return fn({{castToEl % 'val'}}, index{{castToIdx}}) }
        this.forEach(newF)
    }

    /**
     * Creates an {{N}}Array from array-like argument
     *
     * @param o array-like object to initialize {{N}}Array
     *
     * @param mapFn function to apply for each
     *
     * @returns new {{N}}Array
     */
    public from(o: Object, mapFn: (e: Object, index: {{idxType}}) => {{T}}): {{N}}Array {
        throw new Error("{{N}}Array.from: not implemented")
    }
    {%- endfor %}

    /** Underlying ArrayBuffer */
    public readonly buffer: Buffer

    /** Byte offset within the underlying ArrayBuffer */
    public readonly byteOffset: int

    /** Number of bytes used */
    public readonly byteLength: int

    /** Number of {{T}} stored in {{N}}Array */
    public readonly length: int

    /** String \"{{N}}Array\" */
    public readonly name = "{{N}}Array"
}

{%- endfor %}
